package com.hucsmn.examsystem.client;

import com.hucsmn.examsystem.common.AnswerSheet;
import com.hucsmn.examsystem.common.Problem;
import com.hucsmn.examsystem.common.User;
import com.hucsmn.examsystem.common.message.Message;
import com.hucsmn.examsystem.common.message.MessageKind;
import com.hucsmn.examsystem.common.message.Token;

import java.io.*;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.time.Instant;
import java.util.Optional;

/**
 * 在线考试系统客户端。
 */
public class ExamClient {
    // 默认值。

    public static final InetAddress ADDRESS = Inet4Address.getLoopbackAddress();
    public static final int PORT = 8888;
    public static final int TIMEOUT = 10000;

    // 客户端参数。

    private InetAddress address;
    private int port;
    private int timeout;

    // 登录状态。

    private Token loginToken;
    private User loginUser;
    private long loginTime;
    private long loginExpireTime;

    // 构造函数。

    public ExamClient() {
        this(ADDRESS, PORT, TIMEOUT);
    }

    public ExamClient(int port) {
        this(ADDRESS, port, TIMEOUT);
    }

    public ExamClient(InetAddress address, int port) {
        this(address, port, TIMEOUT);
    }

    public ExamClient(InetAddress address, int port, int timeout) {
        setAddress(address);
        setPort(port);
        setTimeout(timeout);
    }

    // RMI 部分。

    private static Message<?> assertSuccess(Message<?> respMsg) throws IOException {
        switch (respMsg.getMessageKind()) {
            case SUCCESS:
                return respMsg;
            case FAIL:
                throw new ExamClientException(Optional.ofNullable(respMsg.getData())
                        .map(Object::toString)
                        .orElse(""));
            default:
                throw new ExamClientException("bad response type");
        }
    }

    public void login(User user, boolean admin) throws IOException {
        if (user == null) {
            throw new NullPointerException();
        }

        Message<?> respMsg = assertSuccess(request(admin ? MessageKind.ADMIN_LOGIN : MessageKind.LOGIN, user));
        loginToken = respMsg.getToken();
        try {
            loginUser = (User) user.clone();
        } catch (CloneNotSupportedException e) {
            loginUser = null;
            e.printStackTrace();
        }
        loginTime = Instant.now().getEpochSecond();
        Object expire = respMsg.getData();
        if (expire instanceof Long) {
            try {
                loginExpireTime = Math.addExact(loginTime, (Long) expire);
            } catch (ArithmeticException e) {
                loginExpireTime = Integer.MAX_VALUE;
            }
        } else {
            loginExpireTime = Integer.MAX_VALUE;
        }
    }

    public void login(boolean admin) throws IOException {
        if (Instant.now().getEpochSecond() < loginExpireTime) {
            return;
        }
        if (loginUser == null) {
            throw new ExamClientException("cannot re-login");
        }
        login(loginUser, admin);
    }

    public void logout() throws IOException {
        assertSuccess(request(MessageKind.LOGOUT));
        loginToken = null;
        loginExpireTime = loginTime;
    }

    public void updateSelf(User user) throws IOException {
        if (user == null) {
            throw new NullPointerException();
        }

        assertSuccess(request(MessageKind.UPDATE_SELF, user));
    }

    public AnswerSheet beginExam() throws IOException {
        Message<?> respMsg = assertSuccess(request(MessageKind.BEGIN_EXAM));
        Object data = respMsg.getData();
        if (!(data instanceof AnswerSheet)) {
            throw new ExamClientException("bad response data type");
        }
        return (AnswerSheet) data;
    }

    public void endExam() throws IOException {
        assertSuccess(request(MessageKind.END_EXAM));
    }

    public AnswerSheet endExam(AnswerSheet answerSheet) throws IOException {
        Message<?> respMsg = assertSuccess(request(MessageKind.END_EXAM, answerSheet));
        Object data = respMsg.getData();
        if (!(data instanceof AnswerSheet)) {
            throw new ExamClientException("bad response data type");
        }
        return (AnswerSheet) data;
    }

    public AnswerSheet recentExam() throws IOException {
        Message<?> respMsg = assertSuccess(request(MessageKind.RECENT_EXAM));
        Object data = respMsg.getData();
        if (!(data instanceof AnswerSheet)) {
            throw new ExamClientException("bad response data type");
        }
        return (AnswerSheet) data;
    }

    public AnswerSheet[] listExams() throws IOException {
        Message<?> respMsg = assertSuccess(request(MessageKind.LIST_EXAMS));
        Object data = respMsg.getData();
        if (!(data instanceof AnswerSheet[])) {
            throw new ExamClientException("bad response data type");
        }
        return (AnswerSheet[]) data;
    }

    public AnswerSheet[] listExams(int uid) throws IOException {
        Message<?> respMsg = assertSuccess(request(MessageKind.LIST_EXAMS, uid));
        Object data = respMsg.getData();
        if (!(data instanceof AnswerSheet[])) {
            throw new ExamClientException("bad response data type");
        }
        return (AnswerSheet[]) data;
    }

    public AnswerSheet[] listExams(String name) throws IOException {
        if (name == null) {
            throw new NullPointerException();
        }

        Message<?> respMsg = assertSuccess(request(MessageKind.LIST_EXAMS, name));
        Object data = respMsg.getData();
        if (!(data instanceof AnswerSheet[])) {
            throw new ExamClientException("bad response data type");
        }
        return (AnswerSheet[]) data;
    }

    public User getUser(int uid) throws IOException {
        Message<?> respMsg = assertSuccess(request(MessageKind.GET_USER, uid));
        Object data = respMsg.getData();
        if (!(data instanceof User)) {
            throw new ExamClientException("bad response data type");
        }
        return (User) data;
    }

    public User getUser(String name) throws IOException {
        if (name == null) {
            throw new NullPointerException();
        }

        Message<?> respMsg = assertSuccess(request(MessageKind.GET_USER, name));
        Object data = respMsg.getData();
        if (!(data instanceof User)) {
            throw new ExamClientException("bad response data type");
        }
        return (User) data;
    }

    public int addUser(User user) throws IOException {
        if (user == null) {
            throw new NullPointerException();
        }

        Message<?> respMsg = assertSuccess(request(MessageKind.ADD_USER, user));
        Object data = respMsg.getData();
        if (!(data instanceof Integer)) {
            throw new ExamClientException("bad response data type");
        }
        return (Integer) data;
    }

    public void updateUser(User user) throws IOException {
        if (user == null) {
            throw new NullPointerException();
        }

        assertSuccess(request(MessageKind.UPDATE_USER, user));
    }

    public void deleteUser(int uid) throws IOException {
        assertSuccess(request(MessageKind.DELETE_USER, uid));
    }

    public void deleteUser(String name) throws IOException {
        if (name == null) {
            throw new NullPointerException();
        }

        assertSuccess(request(MessageKind.DELETE_USER, name));
    }

    public User[] listUsers() throws IOException {
        Message<?> respMsg = assertSuccess(request(MessageKind.LIST_USERS));
        Object data = respMsg.getData();
        if (!(data instanceof User[])) {
            throw new ExamClientException("bad response data type");
        }
        return (User[]) data;
    }

    public Problem getProblem(int pid) throws IOException {
        Message<?> respMsg = assertSuccess(request(MessageKind.GET_PROBLEM, pid));
        Object data = respMsg.getData();
        if (!(data instanceof Problem)) {
            throw new ExamClientException("bad response data type");
        }
        return (Problem) data;
    }

    public int addProblem(Problem problem) throws IOException {
        if (problem == null) {
            throw new NullPointerException();
        }

        Message<?> respMsg = assertSuccess(request(MessageKind.ADD_PROBLEM, problem));
        Object data = respMsg.getData();
        if (!(data instanceof Integer)) {
            throw new ExamClientException("bad response data type");
        }
        return (Integer) data;
    }

    public void updateProblem(Problem problem) throws IOException {
        if (problem == null) {
            throw new NullPointerException();
        }

        assertSuccess(request(MessageKind.UPDATE_PROBLEM, problem));
    }

    public void deleteProblem(int pid) throws IOException {
        assertSuccess(request(MessageKind.DELETE_PROBLEM, pid));
    }

    // 工具方法。

    public Problem[] listProblems() throws IOException {
        Message<?> respMsg = assertSuccess(request(MessageKind.LIST_PROBLEMS));
        Object data = respMsg.getData();
        if (!(data instanceof Problem[])) {
            throw new ExamClientException("bad response data type");
        }
        return (Problem[]) data;
    }

    private Message<?> request(MessageKind kind) throws IOException {
        return request(new Message<>(kind, loginToken));
    }

    private <T extends Serializable> Message<?> request(MessageKind kind, T data) throws IOException {
        return request(new Message<>(kind, loginToken, data));
    }

    private Message<?> request(Message<?> reqMsg) throws IOException {
        if (reqMsg == null) {
            throw new NullPointerException();
        }

        try (Socket sock = new Socket()) {
            sock.setSoTimeout(timeout);
            sock.connect(new InetSocketAddress(address, port));

            ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(sock.getOutputStream()));
            out.writeObject(reqMsg);
            out.flush();

            ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(sock.getInputStream()));
            Object respMsg;
            try {
                respMsg = in.readObject();
            } catch (ClassNotFoundException e) {
                throw new ExamClientException("bad response message");
            }
            if (!(respMsg instanceof Message<?>)) {
                throw new ExamClientException("bad response message");
            }
            return (Message<?>) respMsg;
        }
    }

    // getter 和 setter。

    public User getLoginUser() {
        return loginUser;
    }

    public InetAddress getAddress() {
        return address;
    }

    public void setAddress(InetAddress address) {
        if (address == null) {
            throw new NullPointerException();
        }
        this.address = address;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        if (port < 0) {
            throw new IllegalArgumentException();
        }
        this.port = port;
    }

    public int getTimeout() {
        return timeout;
    }

    public void setTimeout(int timeout) {
        if (timeout < 0) {
            throw new IllegalArgumentException();
        }
        this.timeout = timeout;
    }
}
